package org.bendev.ildaplayer;

import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.SurfaceTexture;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.Surface;
import android.view.TextureView.SurfaceTextureListener;
import android.view.View;
import android.widget.ImageView;

public class SegmentPlayer extends AbstractPlayer implements SurfaceTextureListener {
	static final String TAG = "Player";
	static final String 
		SERVER = "161.105.180.14",
		DATE_FORMAT = "yyyy-MM-dd",
		TIME_FORMAT="kk-mm-ss",
		DATE_TIME_FORMAT = DATE_FORMAT+"-"+TIME_FORMAT;
	static final long SEGMENT_DURATION = android.text.format.DateUtils.MINUTE_IN_MILLIS*2;

	public SegmentPlayer( Context context ){
		this.context = context;
	}
	
	public void setVideoView( VideoView vv ){
		this.vv = vv;
		vv.setSurfaceTextureListener( this );
	}
	
	public void setImageView( ImageView iv ){
		this.iv = iv;
	}
	
	@Override
	public void play( String channel, Calendar pos ){
		synchronized(lock){
			this.channel = channel;
			setStart( pos );
			createSegment();
			getFrame();
		}
	}
	
	@Override
	public void stop(){
		synchronized(lock){
			releaseSegment();
		}
	}
	
	@Override
	public void pause(){
		synchronized(lock){
			if( seg!=null ) seg.pause();
		}
	}
	
	@Override
	public void start(){
		synchronized(lock){
			if( hasSeeked && hasSeekedOut )
				releaseSegment();
			if( seg==null ) createSegment();
			else seg.start();
		}
	}
	
	@Override
	public void seekTo( Calendar pos ){
		synchronized(lock){ 
			setStart( pos );
			hasSeeked = true;
			hasSeekedOut = seg==null || !seg.seekTo(start);
			if( !hasSeekedOut )
				hideFrame();
			else if( isLoadingFrame )
				isPendingSeek = true;
			else{
				isLoadingFrame = true;
				getFrame();
			}
		}
	}
	
	@Override
	public State getState(){
		synchronized(lock){
			return seg==null ? State.UNINITIALIZED : seg.getState();
		}
	}
	
	@Override
	public Calendar getPosition(){
		synchronized(lock){
			return seg==null || hasSeekedOut ? start : seg.getPosition();
		}
	}

	@Override
	public void onSurfaceTextureAvailable( SurfaceTexture surface, int width, int height ){
		Log.i( TAG, "onSurfaceTextureAvailable" );
		synchronized(lock){
			this.surface = surface;
			createSegment();
		}
	}
	
	@Override
	public void onSurfaceTextureSizeChanged( SurfaceTexture surface, int width, int height ){}
	
	@Override
	public boolean onSurfaceTextureDestroyed( SurfaceTexture surface ){ 
		synchronized(lock){ this.surface = null; }
		return false;
	}
	
	@Override
	public void onSurfaceTextureUpdated( SurfaceTexture surface ){}
	
	void setStart( Calendar pos ){
		start = DateUtils.gmt( pos );
	}
	
	void createSegment(){
		if( channel!=null && start!=null && surface!=null && seg==null )
			seg = new Segment( start );
		hasSeeked = hasSeekedOut = false;
	}
	
	void releaseSegment(){
		if( seg!=null ) seg.release();
		seg = null;
	}
	
	void getFrame(){
		RemoteBitmap.get( getFrameUri(), new RemoteBitmap.Callback() {
			@Override
			public void onBitmapError( Exception ex ) {
				Log.e( TAG, "bimap error", ex );
				onBitmapDone();
			}
			
			@Override
			public void onBitmap( Bitmap bm ) {
				if( !hasSeeked || hasSeekedOut ){
					iv.setImageBitmap( bm );
					iv.setVisibility( View.VISIBLE );
				}
				vv.setVideoSize( bm.getWidth(), bm.getHeight() );
				onBitmapDone();
			}

			void onBitmapDone(){
				synchronized(lock){
					if( !isPendingSeek )
						isLoadingFrame = false;
					else{
						getFrame();
						isPendingSeek = false;
					}
				}
			}
		} );		
	}
	
	void hideFrame(){
		iv.setVisibility( View.GONE );
	}

	Uri getFrameUri(){ return Uri.parse( getFrameUri( channel, start ) ); }

	public static String getFrameUri( String channel, Calendar time ){
		Calendar c = (Calendar) time.clone();
		c.set( Calendar.SECOND, 0 );
		c.add( Calendar.MINUTE, -(c.get( Calendar.MINUTE )%2) );
		double pos = ( ( time.getTimeInMillis() - c.getTimeInMillis() )/40 )/25.;
		c.setTimeZone( TimeZone.getTimeZone( "GMT" ) );
		return String.format( Locale.US, "http://%s/mp4/%s/%s/%s.mp4_frame?pos=%.2f&size=half", SERVER, channel, DateFormat.format( DATE_FORMAT, c ), DateFormat.format( DATE_TIME_FORMAT, c ), pos );
	}
	
	final Context context;
	final Object lock = new Object();
	
	boolean isLoadingFrame = false, isPendingSeek = false, hasSeeked = false, hasSeekedOut = false;
	String channel;
	Calendar start;
	VideoView vv;
	ImageView iv;
	SurfaceTexture surface;
	Segment seg;
	
	class Segment{
		final Calendar start;
		final boolean isFirst;
		final String tag;
		final MediaPlayer mediaPlayer;

		long videoDuration = -1;
		int bufferedPercent = -1;
		boolean isAutoStart, isError = false, isBuffered = false, isPrepared = false, isSeeking = false;
		Segment next;
		Object lock = new Object();
		
		public Segment( Calendar start ){
			this( start, 10, true );
		}
		
		Segment( Calendar start, int duration ){
			this( start, duration, false );
		}
		
		Segment( Calendar start, int duration, boolean isFirst ){
			this.start = start;
			this.isFirst = this.isAutoStart = isFirst;
			
			mediaPlayer = new MediaPlayer();
			tag = DateFormat.format( TIME_FORMAT, start ).toString();
			
			mediaPlayer.setOnBufferingUpdateListener( new MediaPlayer.OnBufferingUpdateListener() {
				@Override
				public void onBufferingUpdate( MediaPlayer mp, int percent ) {
					synchronized(lock){
						if( isBuffered ) return;
						bufferedPercent = percent;
						if( percent<100 ) return;
						Log.i( tag, String.format( "buffered" ) );
						isBuffered = true;
					}
					setNext( mediaPlayer.isPlaying() );
				}
			} );
			mediaPlayer.setOnPreparedListener( new MediaPlayer.OnPreparedListener() {
				@Override
				public void onPrepared( MediaPlayer mp ) {
					synchronized(lock){
						isPrepared = true;
						videoDuration = mp.getDuration();
						Log.i( tag, String.format( "prepared, duration=%.2fs (%d ms)", videoDuration/1000., videoDuration ) );
					}
					if( Segment.this.isAutoStart ) startMediaPlayer();
				}
			} );
			mediaPlayer.setOnCompletionListener( new MediaPlayer.OnCompletionListener() {
				@Override
				public void onCompletion( MediaPlayer mp ) {
					Log.i( tag, "complete" );
					mediaPlayer.release();
					if( next!=null ){
						Log.i( tag, "starting next" );
						next.startMediaPlayer();
					}
					synchronized(lock){ seg = next; }
				}
			} );
			mediaPlayer.setOnSeekCompleteListener( new MediaPlayer.OnSeekCompleteListener() {
				@Override
				public void onSeekComplete( MediaPlayer mp ) {
					synchronized(lock){ isSeeking = false; }
				}
			} );
			mediaPlayer.setAudioStreamType( AudioManager.STREAM_MUSIC );

			if( isFirst ){
				mediaPlayer.setSurface( new Surface( surface ) );
				mediaPlayer.setOnVideoSizeChangedListener( new MediaPlayer.OnVideoSizeChangedListener() {
					@Override
					public void onVideoSizeChanged( MediaPlayer mp, int width, int height ) {
						Log.i( tag, String.format( "videoSizeChanged(%d,%d)", width, height ) );
						vv.setVideoSize( width, height );
					}
				} );				
				mediaPlayer.setOnInfoListener( new MediaPlayer.OnInfoListener() {
					@Override
					public boolean onInfo( MediaPlayer mp, int what, int extra ) {
						if( what==MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START ){
							Log.i( tag, "rendering start" );
							onStateChange();
						}
						return true;
					}
				} );
			}
			
			try{
				mediaPlayer.setDataSource( context, getUri( duration ) );
				mediaPlayer.prepareAsync();
			}catch( Exception e ){
				Log.e( tag, "exception", e );
				isError = true;
			}
			if( isFirst )
				onStateChange();
		}
		
		public void start() {
			mediaPlayer.start();
			onStateChange();
		}
		
		public void pause(){
			mediaPlayer.pause();
			onStateChange();
		}
		
		public boolean seekTo( Calendar pos ){
			long offset = pos.getTimeInMillis() - start.getTimeInMillis(), maxOffset = (videoDuration*bufferedPercent)/100;
			if( offset < 0 )
				return false;
			if( offset > maxOffset ){
				boolean res = isBuffered && next!=null && next.seekTo( pos );
				if( res ){
					next.startMediaPlayer();
					seg = next;
				}
				return res;
			}
			try{
				synchronized(lock){
					if( !isSeeking ){
						isSeeking = true;
						mediaPlayer.seekTo( (int) offset );
					}
				}
				return true;
			}catch( IllegalStateException e ){
				return false;
			}
		}
		
		public State getState(){
			synchronized( lock ) { return isError ? State.PLAYBACK_ERROR : !isPrepared ? State.BUFFERING : mediaPlayer.isPlaying() ? State.PLAYING : State.PAUSED; }
		}
		
		public Calendar getPosition(){
			return DateUtils.add( start, mediaPlayer.getCurrentPosition() );
		}
		
		public void release(){
			mediaPlayer.reset();
			mediaPlayer.release();
			if( next!=null )
				next.release();
		}
		
		void startMediaPlayer(){
			if( !isAutoStart ){
				mediaPlayer.setSurface( new Surface( surface ) );
				synchronized(lock){
					if( !isPrepared ){
						isAutoStart = true;
						return;
					}
				}
			}
			else{
				iv.setVisibility( View.GONE );
			}
			mediaPlayer.start();
			setNext( true );
		}
		
		void setNext( boolean isStarted ){
			if( next==null && isPrepared && isBuffered && (isAutoStart || isStarted) ){
				Calendar nextStart = DateUtils.add( start, videoDuration );
				if( isFirst )
					next = new Segment( nextStart, -30 );
				else
					next = new Segment( DateUtils.round( nextStart, SEGMENT_DURATION ), 0 );
			}
		}
		
		Uri getUri( int duration ){
			String uri = String.format( "http://%s/mp4/%s/%s.mp4%s", SERVER, channel, DateFormat.format( DATE_TIME_FORMAT, start ), duration!=0 ? String.format( "?%d", duration ) : "" );
			Log.i( tag, String.format("setDataSource(%s)", uri ) );
			return Uri.parse( uri );
		}
	}
}
